/*
 * Copyright 2009 Andreas Borglin andreas.borglin@gmail.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package borge.mobile.app.jakoon;

import borge.mobile.lib.pimp.ImageConstants;
import borge.mobile.lib.pimp.ImageManager;
import borge.mobile.lib.pimp.ImageSupport;
import borge.mobile.lib.pimp.OverlayBounds;
import java.io.InputStream;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;

/**
 * Canvas class that displays the image and the options
 * @author Andreas Borglin
 */
public class ImageCanvas extends Canvas implements ImageConstants, PaintListener, MouseListener, MouseMoveListener {

    // Indexes for menu options
    private static final int EXIT_INDEX = 0;
    private static final int SAVE_INDEX = 1;
    //private static final int UPLOAD_INDEX = 2;
    private static final int STARTOVER_INDEX = 2;
    private static final int UNDO_INDEX = 3;
    private static final int MENU_ITEMS = UNDO_INDEX + 1;

    // Internal classes
    private ImageForm imageForm;
    private CanvasUtil util;

    // eSWT classes
    private Display disp;
    
    // Imageeffect library classes
    private ImageSupport support;
    private ImageManager preview;
    private OverlayBounds overlayBounds;

    // Canvas size
    private int canvasWidth;
    private int canvasHeight;

    // Image related
    private Image image;
    private Rectangle imageBounds;
    
    // Booleans related to drawing state
    private boolean drawCommandChoices;
    private boolean drawMenu = false;
    private boolean drawCommands = false;
    private boolean actionMode = false;

    // All animations have been removed due to performance issues
    //private boolean animationMode = false;

    // Booleans for current mouse state
    private boolean mouseDownDraw = false;
    private boolean mouseUpDraw = false;

    // Other boolean values
    private boolean drawingOutsideImage = false;
    private boolean bottomFormLoaded = false;

    // Coordinate trackers and helpers
    private int curY = 0;
    private int menuY = 0;
    private int[] curArrY;
    private static final int menuStartX = 10;
    private int menuEndX;
    private int menuStartY;
    private int[] startY;
    private static final int commandsStartX = 480;
    private static final int ySpacing = 42;
    private int[] menuYs;
    
    // Info strings
    private String info;
    private static final String noMenu = "Tap screen for menu";
    private static final String drawRect = "Draw rectangle from top left point";
    private static final String outsideImage = "Drawing outside of image!";

    // Operations string values
    private String[] operations;

    // Image arrays
    private Image[] cmdImages;
    private Image[] menuImages;
    private Image[] curImgArray;
    private Image[] effectImages;
    private Image[] rotateImages;
    private Image[] flipImages;
    private Image bullet;

    // Alpha transparency values
    private static final int OPAQUE = 255;
    private static final int SEMI_TRANSPARENT = 100;

    private Color whiteColor;
    private Color blackColor;

    /**
     * Constructor
     * @param comp Composite this canvas belongs to
     * @param d Display used to initialize UI stuff
     * @param imgForm The form owning this canvas
     */
    protected ImageCanvas(Composite comp, Display d, ImageForm imgForm) {
        super(comp, SWT.NONE);
        disp = d;
        addPaintListener(this);
        addMouseListener(this);
        addMouseMoveListener(this);
        imageForm = imgForm;
        
        operations = new String[OPERATIONS];
        operations[ROTATE] = "Rotate";
        operations[FLIP] = "Flip";
        operations[CROP] = "Crop";
        operations[FRAME] = "Frame";
        operations[BRIGHTNESS] = "Brightness";
        operations[CONTRAST] = "Contrast";
        operations[EFFECT] = "Effects";
        // These are supported by the framework, but have been removed here
        // since they have a low usecase
        operations[WHITEBALANCE] = null; //"WhiteBalance";
        operations[OVERLAY] = null; //"Overlay";
        operations[GAMMA] = null; //"Gamma";
        // Resize is moved to the Save form
        operations[RESIZE] = null; //"Resize";
        
        
        util = new CanvasUtil();
        
        startY= new int[operations.length];

        // Command image array
        cmdImages = new Image[operations.length];
        cmdImages[ROTATE] = new Image(disp, this.getClass().getResourceAsStream("/images/rotate.gif"));
        cmdImages[FLIP] = new Image(disp, this.getClass().getResourceAsStream("/images/flip.gif"));
        cmdImages[CROP] = new Image(disp, this.getClass().getResourceAsStream("/images/crop.gif"));
        cmdImages[FRAME] = new Image(disp, this.getClass().getResourceAsStream("/images/frame.gif"));
        cmdImages[EFFECT] = new Image(disp, this.getClass().getResourceAsStream("/images/effects.gif"));
        cmdImages[BRIGHTNESS] = new Image(disp, this.getClass().getResourceAsStream("/images/brightness.gif"));
        cmdImages[CONTRAST] = new Image(disp, this.getClass().getResourceAsStream("/images/contrast.gif"));
        cmdImages[RESIZE] = new Image(disp, this.getClass().getResourceAsStream("/images/resize.gif"));

        // Menu image array
        // TODO: Enable Upload when support has been added
        menuImages = new Image[MENU_ITEMS];
        menuImages[EXIT_INDEX] = new Image(disp, this.getClass().getResourceAsStream("/images/exit.gif"));
        menuImages[SAVE_INDEX] = new Image(disp, this.getClass().getResourceAsStream("/images/save.gif"));
        //menuImages[UPLOAD_INDEX] = new Image(disp, this.getClass().getResourceAsStream("/images/upload.gif"));
        menuImages[STARTOVER_INDEX] = new Image(disp, this.getClass().getResourceAsStream("/images/startover.gif"));
        menuImages[UNDO_INDEX] = new Image(disp, this.getClass().getResourceAsStream("/images/undo.gif"));
        menuYs = new int[menuImages.length];

        // Image effects array
        effectImages = new Image[5];
        effectImages[0] = new Image(disp, this.getClass().getResourceAsStream("/images/monochrome.gif"));
        effectImages[1] = new Image(disp, this.getClass().getResourceAsStream("/images/negative.gif"));
        effectImages[2] = new Image(disp, this.getClass().getResourceAsStream("/images/emboss.gif"));
        effectImages[3] = new Image(disp, this.getClass().getResourceAsStream("/images/sepia.gif"));
        effectImages[4] = new Image(disp, this.getClass().getResourceAsStream("/images/solarize.gif"));

        flipImages = new Image[2];
        flipImages[0] = new Image(disp, this.getClass().getResourceAsStream("/images/horizontal.gif"));
        flipImages[1] = new Image(disp, this.getClass().getResourceAsStream("/images/vertical.gif"));

        rotateImages = new Image[3];
        rotateImages[0] = new Image(disp, this.getClass().getResourceAsStream("/images/90degrees.gif"));
        rotateImages[1] = new Image(disp, this.getClass().getResourceAsStream("/images/180degrees.gif"));
        rotateImages[2] = new Image(disp, this.getClass().getResourceAsStream("/images/270degrees.gif"));

        // Used to differentiate commands from image choices
        bullet = new Image(disp, this.getClass().getResourceAsStream("/images/bulletrect.gif"));
        
        // Commonly used colors
        whiteColor = new Color(disp, 255, 255, 255);
        blackColor = new Color(disp, 0, 0, 0);

        calcMenuEndX();
    }

    /**
     * Reset canvas for starting over with new image
     */
    protected void reset(){
        drawMenu = false;
        drawCommands = false;
        drawCommandChoices = false;
        actionMode = false;
        image = null;
        resetDataState(true);
        redraw();
        update();
    }

    /**
     * Reset data state
     */
    private void resetDataState(boolean cmdMode){
        if(cmdMode)
            Data.commandMode = NO_VALUE;
        Data.mouseDownX = NO_VALUE;
        Data.mouseDownY = NO_VALUE;
        Data.mouseDragX = NO_VALUE;
        Data.mouseDragY = NO_VALUE;
    }

    /**
     * Set image support from library
     */
    protected void setSupport(ImageSupport support){
        this.support = support;
        
    }

    /**
     * Set the image manager
     */
    protected void setImageManager(ImageManager im){
        preview = im;
    }

    /**
     * Calculate where the menu ends for click calculations
     */
    private void calcMenuEndX(){
        
        // calculate where the menu ends x-wise
        if(menuEndX == 0){
            // All images are equal size, so just get it from one of them
            menuEndX = menuImages[EXIT_INDEX].getBounds().width;
            menuEndX += menuStartX + 5;
            
        }
    }

    /**
     * Override Canvas.paintControl to paint canvas
     */
    public void paintControl(PaintEvent e){
        
        canvasWidth = e.width;
        canvasHeight = e.height;
        
        try{
            
            // Draw image or loading string
            if(image != null){
                drawImage(e.gc);
            }
            else{
                e.gc.setForeground(whiteColor);
                e.gc.drawString("Loading and scaling image...", 5, 5);
                return;
            }

            // Draw info strings
            drawStrings(e.gc);
           
            // Draw the menu
            if(drawMenu){
                drawMenu(e.gc);
            }

            // Draw commands
            if(drawCommands)
                drawCommands(e.gc);
            
            // Draw the command choices
            if(drawCommandChoices){
                drawCommandChoices(e.gc);
            }
            
            // If dragging rectangles during drag
            else if(util.inUserDrawRectMode() && Data.mouseDownX != NO_VALUE && Data.mouseDragX != NO_VALUE){
                drawRectangles(e.gc);
            }

            // If mouse down and we draw info for rotate flip
            else if(mouseDownDraw && util.inUserDrawLineMode()){
                  drawLineDragInfo(e.gc);
            }
            
        }
        catch(Exception ee){
            ee.printStackTrace();
        }
        
    }
    
    // ### BEGIN HELPER FUNCTIONS FOR paintControl


    /**
     * Draw info strings
     */
    private void drawStrings(GC gc){

        gc.setAlpha(SEMI_TRANSPARENT);
        gc.setForeground(whiteColor);

        int scale = preview.getPreviewScaleRatio();
        int width = preview.getCurrentRealWidth();
        int height = preview.getCurrentRealHeight();
        String show = scale + "% of " + width + "x" + height + " pixels";

        gc.drawString(show, 5, 5);

        info = null;

        if(!drawMenu && !drawCommandChoices)
            info = noMenu;

        if(actionMode && Data.commandMode == CROP || Data.commandMode == FRAME)
            info = drawRect;

        if(actionMode && util.inUserDrawLineMode())
            info = "Press center of screen and drag to choice";

        if(drawingOutsideImage){
            info = outsideImage;
            drawingOutsideImage = false;
        }

        if(info != null)
            gc.drawString(info, 280, 5);
    }


    /**
     * Draw the image
     */
    private void drawImage(GC gc){
        
        // Calculate to place image in middle
        int cw = getWidth();
        int iw = image.getBounds().width;
        int xpos = (cw - iw) / 2;

        int ch = getHeight();
        int ih = image.getBounds().height;
        int ypos = (ch - ih) / 2;

        gc.setAlpha(OPAQUE);
        gc.drawImage(image, xpos, ypos);

        // Keep bounds for coordinate calculations
        imageBounds = new Rectangle(xpos, ypos, iw, ih);
        Data.imageStartX = xpos;
        Data.imageStartY = ypos;

    }

    /**
     * Draw menu
     */
    private void drawMenu(GC gc){
        
        for (int i = 0; i < menuImages.length; ++i) {
            gc.drawImage(menuImages[i], menuStartX, menuY - (ySpacing * i));
            menuYs[i] = menuY - (ySpacing * i);
        }

        menuStartY = menuYs[menuYs.length - 1];
        
    }

    /**
     * Draw commands
     */
    private void drawCommands(GC gc){
        
        for (int i = 0, yc = 0; i < operations.length; ++i) {
            
            if(operations[i] == null){
                startY[i] = NO_VALUE;
                continue;
            }

            gc.drawImage(cmdImages[i], commandsStartX, curY - (yc * ySpacing));
            startY[i] = curY - (yc * ySpacing);
            yc++;
        }
    }

    /**
     * Draw choices for a command
     */
    private void drawCommandChoices(GC gc){

        final int yPadding = 10;
        gc.drawImage(cmdImages[Data.commandMode], commandsStartX, yPadding);
        for (int i = 0; i < curImgArray.length; ++i) {
            int y = yPadding + ySpacing + (i * ySpacing);
            curArrY[i] = y;
            gc.drawImage(curImgArray[i], commandsStartX + 20, y);
            // Compensate for bullet not in middle of image
            gc.drawImage(bullet, commandsStartX + 4, y + 14);
        }

        drawCommandChoices = false;
    }

    /**
     * Draw rectangle for selection in image used for crop or frame.
     */
    private void drawRectangles(GC gc){

        if (actionMode) {
            gc.setAlpha(OPAQUE);
            gc.setForeground(whiteColor);
            // Draw a rectangle to highlight an area of the image
            if (Data.commandMode == CROP || Data.commandMode == FRAME) {
                gc.drawRectangle(Data.mouseDownX, Data.mouseDownY, Data.mouseDragX - Data.mouseDownX, Data.mouseDragY - Data.mouseDownY);
                gc.setForeground(blackColor);
                gc.drawRectangle(Data.mouseDownX-1, Data.mouseDownY-1, Data.mouseDragX - Data.mouseDownX+2, Data.mouseDragY - Data.mouseDownY+2);
            }
            // Draw a filled rectangle to show where overlay image will be saved
            else if (Data.commandMode == OVERLAY) {
                gc.fillRectangle(Data.mouseDragX, Data.mouseDragY, overlayBounds.getWidth(), overlayBounds.getHeight());
            }
        }
    }

    /**
     * Draw a coordinate system for rotating and flipping
     * Currently replaced by a list of commands instead
     */
    private void drawLineDragInfo(GC gc){
    
        if (actionMode) {
            gc.setAlpha(OPAQUE);
            gc.setForeground(blackColor);

            int size = 100;
            gc.drawLine(Data.mouseDownX-size, Data.mouseDownY-1, Data.mouseDownX+size, Data.mouseDownY-1);
            gc.drawLine(Data.mouseDownX-1, Data.mouseDownY-size, Data.mouseDownX-1, Data.mouseDownY+size);

            gc.setForeground(whiteColor);
            gc.drawLine(Data.mouseDownX-size, Data.mouseDownY, Data.mouseDownX+size, Data.mouseDownY);
            gc.drawLine(Data.mouseDownX, Data.mouseDownY-size, Data.mouseDownX, Data.mouseDownY+size);

            gc.setAlpha(SEMI_TRANSPARENT);
            gc.drawString("90",Data.mouseDownX+size , Data.mouseDownY+30);
            gc.drawString("270",Data.mouseDownX-size , Data.mouseDownY-30);
            gc.drawString("180",Data.mouseDownX+10 , Data.mouseDownY+size);
            gc.drawString("180",Data.mouseDownX+10 , Data.mouseDownY-size);

        }

    }
    
    // ### END HELPER FUNCTIONS FOR paintControl
    
    
    /**
     * TODO: Move this to ImageForm?
     */
    private class StartOver implements Runnable{
        public void run(){
            imageForm.initImageHandler(true);
        }
    }

    /**
     * Handle menu choices
     */
    private void handleMenu(int choice){

        switch(choice){
            case EXIT_INDEX:
                imageForm.exit();
                break;
            case SAVE_INDEX:
                imageForm.save();
                break;
            /*case UPLOAD_INDEX: Not currently supported
                imageForm.upload();
                break;*/
            case STARTOVER_INDEX:
                disp.syncExec(new StartOver());
                break;
            case UNDO_INDEX:
                imageForm.undo();
                break;
            default: break;
        }
        
        
    }
    
    protected void bottomFormUnloaded(){
        bottomFormLoaded = false;
    }

    /**
     * Implements MouseListener mouseUp
     */
    public void mouseUp(MouseEvent e) {

        // If illegal mouseDown, nothing to do here
        if(Data.mouseDownX == NO_VALUE)
            return;

        mouseDownDraw = false;
        mouseUpDraw = true;

        // If menu has been drawn, we've not already executed a command
        // and x coord is in the command area
        if(drawCommands && !actionMode && e.x > commandsStartX){
           handleCommandClick(e.x, e.y);
           return;
        }
        
        // If menu click (undo, start over....)
        else if(drawMenu && e.x > menuStartX && e.x < menuEndX && e.y > menuStartY){
            handleMenuClick(e.x, e.y);
            return;
        }
        
        else if(actionMode){
            if(util.inUserDrawMode() || e.x > commandsStartX){
                handleActionClick(e.x, e.y);
                return;
            }
            else
                actionMode = false; // fall through to menu
        }

        resetDataState(false);
        
        // Sync menu and command at this point
        drawCommands = drawMenu;
        drawMenu = !drawMenu;
        drawCommands = !drawCommands;
        if(bottomFormLoaded)
            imageForm.bottomFormCallback(50);
        
        // Draw
        if (drawMenu) {
            curY = menuY = 310;
            redraw();
        } else {
            redraw();
        }
    }
   
    /**
     * Handle a click in menu region
     */
    private void handleMenuClick(int x, int y){
        
        if (util.inSameArea(x, Data.mouseDownX) && util.inSameArea(y, Data.mouseDownY)) {

            for (int i = 0; i < menuYs.length; ++i) {
                if ((i == 0 && y > menuYs[i]) ||
                        (y > menuYs[i] && y < menuYs[i - 1])) {
                    handleMenu(i);
                }
            }
        }
    }

    /**
     * Handle a click in command region
     */
    private void handleCommandClick(int x, int y){
        
        if (util.inSameArea(x, Data.mouseDownX) && util.inSameArea(y, Data.mouseDownY)) {

            boolean first = true;
            for (int i = 0; i < startY.length; ++i) {

                if(startY[i] == NO_VALUE)
                    continue;
                
                if ((first && y > startY[i]) ||
                        (y > startY[i] && y < startY[i - 1])) {
                    Data.commandMode = i;
                    actionMode = true;
                    handleCommandAction();
                    return;
                }
                first = false;

            }
        }
    }

    /**
     * Handle a command
     */
    private void handleCommandAction(){

        if(util.inUserDrawMode()){

            if(Data.commandMode == OVERLAY)
                overlayBounds = imageForm.loadOverlayImage();
            drawMenu = false;
            drawCommands = false;
            redraw();
            return;
        }

        if(util.inExternalHandlerMode()){

            drawCommands = drawMenu = false;
            redraw();
            imageForm.loadBottomForm();
            bottomFormLoaded = true;
            return;
        }

        switch(Data.commandMode){
            case ROTATE: curImgArray = rotateImages; break;
            case FLIP: curImgArray = flipImages; break;
            case EFFECT: curImgArray = effectImages; break;
        }

        curArrY = new int[curImgArray.length];
        drawCommandChoices = true;
        drawCommands = false;
        redraw();

    }

    /**
     * Handle a command choice action
     */
    private void handleActionClick(int x, int y){
        
        if (util.inChoiceArrayMode()) {

            if (util.inSameArea(x, Data.mouseDownX) &&
                    util.inSameArea(y, Data.mouseDownY)) {

                for (int i = 0; i < curArrY.length; ++i) {

                    if ((i == (curArrY.length - 1) && y > curArrY[i]) ||
                            (y > curArrY[i] && y < curArrY[i + 1])) {
                        drawMenu = false;
                        actionMode = false;

                        imageForm.applyChange(i);
                        break;
                    }
                }
            }
        } 
        
        else if (util.inUserDrawMode()) {

            boolean apply = true;
            if(util.inUserDrawLineMode()){

                int xdiff = Math.abs(Data.mouseDragX - Data.mouseDownX);
                int ydiff = Math.abs(Data.mouseDragY - Data.mouseDownY);

                if(xdiff < 20 && ydiff < 20){
                    apply = false;
                }

            }
            if(apply)
                imageForm.applyChange(0); // int parameter ignored here

            resetDataState(false);
        }

        actionMode = false;
        Data.commandMode = NO_VALUE;
    }

    /**
     * Implement MouseListener mouseDown
     */
    public void mouseDown(MouseEvent e){

        // Ignore if we are in drag mode and user does stuff outside of image
        if(actionMode && util.inUserDrawMode()){

            if(e.x < imageBounds.x || e.x > (imageBounds.x + imageBounds.width))
                drawingOutsideImage = true;

            if(e.y < imageBounds.y || e.y > (imageBounds.y + imageBounds.height))
                drawingOutsideImage = true;

            if(drawingOutsideImage){
                resetDataState(true);
                redraw();
                return;
            }
        }

        resetDataState(false);
        Data.mouseDownX = e.x;
        Data.mouseDownY = e.y;
        mouseUpDraw = false;
        mouseDownDraw = true;

        if(util.inUserDrawLineMode())
            redraw();
        
    }


    /**
     * Implement MouseListener mouseMove
     */
    public void mouseMove(MouseEvent e){
       
        Data.mouseDragX = e.x;
        Data.mouseDragY = e.y;
         
        if(Data.mouseDownX != NO_VALUE){
            
            switch(Data.commandMode){
                
                case OVERLAY:
                case FRAME:
                case CROP: 
                { 
                    redraw();
                    break;
                }
                case ROTATE:
                case FLIP: {
                    //redraw();
                    break;
                }
            }
            
        }
    }

    /**
     * Implement MouseListener mouseDoubleClick
     * Ignored in this application
     */
    public void mouseDoubleClick(MouseEvent e){
        System.out.println("mouseDoubleClick: " + e.x + " " + e.y);
    }

    /**
     * Get canvas width
     * @return canvas width
     */
    public int getWidth(){
        return canvasWidth;
    }

    /**
     * Get canvas height
     * @return canvas height
     */
    public int getHeight(){
        return canvasHeight;
    }

    /**
     * Create image from a stream
     * @param is Stream with image data
     */
    public void setImage(InputStream is){
        
        try {
            image = new Image(disp, is);
        } 
        
        catch (Exception ee) {
            ee.printStackTrace();
        }
    }

    /**
     * Get the current image object
     */
    protected Image getCurrentImage(){
        return image;
    }
}
